using System.Collections.Immutable;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Everywhere.Configuration.SourceGenerator;

[Generator(LanguageNames.CSharp)]
public sealed class SettingsItemsSourceGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Find classes annotated with [GeneratedSettingsItems]
        var candidates = context.SyntaxProvider.CreateSyntaxProvider(
                static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
                static (ctx, _) =>
                {
                    if (ctx.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)ctx.Node) is not INamedTypeSymbol symbol) return null;
                    return symbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == KnownAttributes.GeneratedSettingsItems) ?
                        symbol :
                        null;
                })
            .Where(static s => s is not null);

        var grouped = context.CompilationProvider.Combine(candidates.Collect());
        context.RegisterSourceOutput(
            grouped,
            static (spc, tuple) =>
            {
                foreach (var t in tuple.Right.Distinct(SymbolEqualityComparer.Default).OfType<INamedTypeSymbol>())
                {
                    EmitForType(spc, t);
                }
            });
    }

    private static void EmitForType(in SourceProductionContext ctx, INamedTypeSymbol type)
    {
        if (!type.IsPartial())
        {
            // Report diagnostic for not being a partial class
            ctx.ReportDiagnostic(
                Diagnostic.Create(
                    Diagnostics.MustBePartial,
                    type.Locations.FirstOrDefault(),
                    type.Name));
            return;
        }

        // Collect metadata for all relevant properties of the class
        var members = type.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p is { IsStatic: false, GetMethod: not null, IsImplicitlyDeclared: false })
            .Where(p => !p.IsHiddenItem())
            .Select(p => BuildPropertyMetadata(p, p, p.Name))
            .ToImmutableArray();

        var ns = type.GetNamespace();
        var sb = new IndentedStringBuilder();
        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable").AppendLine();
        sb.AppendLine("using System.Reflection;").AppendLine(); // we need this for enum item generation

        if (ns.Length > 0)
        {
            sb.Append("namespace ").Append(ns).AppendLine(";");
            sb.AppendLine();
        }

        sb.AppendLine("[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(");
        sb.AppendLine("    global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties |");
        sb.AppendLine("    global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors");
        sb.AppendLine(")]");
        sb.Append("partial class ").Append(type.Name).AppendLine();
        sb.AppendLine("{");
        using (sb.Indent())
        {
            sb.AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]");
            sb.AppendLine("[global::System.Text.Json.Serialization.JsonIgnore]");
            sb.AppendLine("public global::Everywhere.Configuration.SettingsItems SettingsItems { get; } = new();").AppendLine();

            // Partial method for further customization
            sb.AppendLine("partial void OnConstructed();").AppendLine();

            // Constructor
            sb.Append("public ").Append(type.Name).AppendLine("()");
            sb.AppendLine("{");
            using (sb.Indent())
            {
                sb.AppendLine("OnConstructed();").AppendLine();

                foreach (var meta in members)
                {
                    if (string.IsNullOrWhiteSpace(meta.HeaderKey))
                    {
                        // Report diagnostic for missing DynamicResourceKey
                        ctx.ReportDiagnostic(
                            Diagnostic.Create(
                                Diagnostics.EmptyHeaderKey,
                                meta.Symbol.Locations.FirstOrDefault(),
                                meta.Name));
                    }

                    EmitItemRecursive(ctx, sb, meta, $"item_{meta.Name.Replace(".", "_")}", meta.Name, "SettingsItems");
                }
            }
            sb.AppendLine("}");
        }
        sb.AppendLine("}");

        ctx.AddSource($"{type.Name}.SettingsItems.g.cs", sb.ToString());
    }

    /// <summary>
    /// A settings item may be nested, e.g. Customizable`, so we use recursion here.
    /// </summary>
    /// <param name="ctx"></param>
    /// <param name="sb"></param>
    /// <param name="metadata"></param>
    /// <param name="itemName"></param>
    /// <param name="bindingPath"></param>
    /// <param name="parentCollection"></param>
    private static void EmitItemRecursive(
        in SourceProductionContext ctx,
        IndentedStringBuilder sb,
        PropertyMetadata metadata,
        string itemName,
        string bindingPath,
        string? parentCollection)
    {
        switch (metadata.Kind)
        {
            case ItemKind.Selection:
            {
                if (metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsSelectionItem) is not { } attribute)
                {
                    // This case should ideally not be hit if Classify is correct
                    return;
                }

                var itemsSourceBindingPath = attribute.GetNamedArgument("ItemsSourceBindingPath") switch
                {
                    { IsNull: false, Value: string path } => path,
                    _ => attribute.ConstructorArguments.Length > 0 && attribute.ConstructorArguments[0].Value is string ctorPath ?
                        ctorPath :
                        string.Empty
                };

                if (string.IsNullOrEmpty(itemsSourceBindingPath))
                {
                    // Report diagnostic for missing ItemsSourceBindingPath
                    ctx.ReportDiagnostic(
                        Diagnostic.Create(
                            Diagnostics.MissingItemsSourceBindingPath,
                            metadata.Symbol.Locations.FirstOrDefault(),
                            metadata.Name));
                    break;
                }

                // We need to figure out the symbol of `itemsSourceBindingPath`
                var itemsSourceSymbol = metadata.Symbol;
                var parts = itemsSourceBindingPath.Split('.');
                foreach (var part in parts)
                {
                    if (itemsSourceSymbol is null) break;

                    var trimmedPart = part.Trim();
                    itemsSourceSymbol = itemsSourceSymbol.ContainingType?.GetMembers().FirstOrDefault(m => m.Name == trimmedPart);
                }

                if (itemsSourceSymbol is null)
                {
                    ctx.ReportDiagnostic(
                        Diagnostic.Create(
                            Diagnostics.InvalidItemsSourceBindingPath,
                            metadata.Symbol.Locations.FirstOrDefault(),
                            metadata.Name));
                    break;
                }

                var itemSourceType = itemsSourceSymbol switch
                {
                    IPropertySymbol prop => prop.Type,
                    IFieldSymbol field => field.Type,
                    _ => null
                };
                if (itemSourceType is null)
                {
                    ctx.ReportDiagnostic(
                        Diagnostic.Create(
                            Diagnostics.InvalidItemsSourceBindingPath,
                            metadata.Symbol.Locations.FirstOrDefault(),
                            metadata.Name));
                    break;
                }

                sb.AppendLine($"var {itemName} = new global::Everywhere.Configuration.SettingsSelectionItem();");
                sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsSelectionItem.ItemsSourceProperty] = ");

                var converterBuilder = new IndentedStringBuilder();
                converterBuilder.Append("new global::Avalonia.Data.Converters.FuncValueConverter<");
                converterBuilder.Append(itemSourceType.ToDisplayString());
                converterBuilder.AppendLine(
                    ", global::System.Collections.Generic.IEnumerable<global::Everywhere.Configuration.SettingsSelectionItem.Item>?>(x =>");
                converterBuilder.AppendLine("{");
                using (converterBuilder.Indent())
                {
                    converterBuilder.AppendLine("if (x is null) return null;");
                    converterBuilder.AppendLine("global::Avalonia.Controls.Templates.IDataTemplate? contentTemplate = null;");

                    var dataTemplateKeyExpr = attribute.GetNamedArgument("DataTemplateKey") switch
                    {
                        { Kind: TypedConstantKind.Type, Value: INamedTypeSymbol typeSymbol } =>
                            $"typeof({typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})",
                        { IsNull: false, Value: string str } => str,
                        _ => null
                    };

                    if (dataTemplateKeyExpr is not null)
                    {
                        converterBuilder.AppendLine(
                            $$"""
                              if (global::Avalonia.Application.Current?.Resources.TryGetResource({{dataTemplateKeyExpr}}, null, out var resource) is true)
                              {
                                  contentTemplate = resource as global::Avalonia.Controls.Templates.IDataTemplate;
                              }
                              """);
                    }

                    var transformExpr = GetNamedArgValue(attribute, "I18N", "false") == "true" ?
                        $"DynamicResourceKey($\"SettingsSelectionItem_{metadata.Symbol.ContainingType.Name}_{metadata.Name}_{{k}}\")" :
                        "DirectResourceKey(k)";

                    // If the type is INotifyPropertyChanged and IEnumerable<T>, use ToObservableChangeSet
                    if (itemSourceType.AllInterfaces.Any(i => i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable<T>" &&
                            itemSourceType.AllInterfaces.Any(ii => ii.ToDisplayString() == "System.ComponentModel.INotifyPropertyChanged")))
                    {
                        converterBuilder.AppendLine("global::DynamicData.ObservableListEx.Bind(");
                        using (converterBuilder.Indent())
                        {
                            converterBuilder.AppendLine("global::DynamicData.ObservableListEx.Transform(");
                            using (converterBuilder.Indent())
                            {
                                converterBuilder.AppendLine("global::DynamicData.Binding.ObservableCollectionEx.ToObservableChangeSet(x),");
                                converterBuilder.Append(
                                    "k => new global::Everywhere.Configuration.SettingsSelectionItem.Item(new global::Everywhere.I18N.");
                                converterBuilder.Append(transformExpr).Append(", ");
                                converterBuilder.AppendLine("k, contentTemplate)), ");
                            }
                            converterBuilder.AppendLine("out var result).Subscribe();");
                        }
                        converterBuilder.AppendLine("return result;");
                    }
                    else
                    {
                        converterBuilder.Append("return x.OfType<");
                        converterBuilder.Append(metadata.Type.ToDisplayString(NullableFlowState.NotNull));
                        converterBuilder.Append(
                            ">().Select(k => new global::Everywhere.Configuration.SettingsSelectionItem.Item(new global::Everywhere.I18N.");
                        converterBuilder.Append(transformExpr);
                        converterBuilder.AppendLine(", k, contentTemplate)).ToList();");
                    }
                }

                EmitBinding(
                    sb,
                    itemsSourceBindingPath,
                    BindingMode.OneWay,
                    converterBuilder.AppendLine("})").ToString());
                sb.AppendLine(";");
                break;
            }
            case ItemKind.Customizable:
            {
                var innerType = ((INamedTypeSymbol)metadata.Type).TypeArguments[0];
                var innerMetadata = BuildPropertyMetadata(metadata.Symbol, metadata.AttributeOwner, metadata.Name, innerType);

                var innerItemName = itemName + "_inner";
                // Recursive call for the wrapped item
                EmitItemRecursive(
                    in ctx,
                    sb,
                    innerMetadata,
                    innerItemName,
                    $"{bindingPath}.BindableValue",
                    null);

                sb.AppendLine($"var {itemName} = new global::Everywhere.Configuration.SettingsCustomizableItem({innerItemName});");
                sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsCustomizableItem.ResetCommandProperty] = ");
                EmitBinding(sb, $"{bindingPath}.ResetCommand", BindingMode.OneWay).AppendLine(";");

                // Special case from reflection code: set watermark for string properties
                if (innerMetadata.Kind == ItemKind.String)
                {
                    sb.Append($"{innerItemName}[!global::Everywhere.Configuration.SettingsStringItem.WatermarkProperty] = ");
                    EmitBinding(sb, $"{bindingPath}.DefaultValue", BindingMode.OneWay).AppendLine(";");
                }
                break;
            }
            case ItemKind.SettingsControl:
            {
                // In this case, we need to call the property itself to get the ISettingsControl instance
                // Then cast it to ISettingsControl and call CreateControl()
                // Next, set the DataContext of the created control to 'this'
                // Finally, assign it to a new SettingsControlItem

                // If the property is nullable, Report diagnostic
                if (metadata.Type.NullableAnnotation == NullableAnnotation.Annotated)
                {
                    ctx.ReportDiagnostic(
                        Diagnostic.Create(
                            Diagnostics.NullableSettingsControl,
                            metadata.Symbol.Locations.FirstOrDefault(),
                            metadata.Name));
                    return;
                }

                sb.AppendLine($"global::System.Func<global::Avalonia.Controls.Control> control_{itemName}_factory = () =>").AppendLine("{");
                using (sb.Indent())
                {
                    sb.AppendLine(
                        $"var control_{itemName} = ((global::Everywhere.Configuration.ISettingsControl)this.{metadata.Name}).CreateControl();");
                    sb.AppendLine($"control_{itemName}.DataContext = this;");
                    sb.AppendLine($"return control_{itemName};");
                }
                sb.AppendLine("};");
                sb.AppendLine($"var {itemName} = new global::Everywhere.Configuration.SettingsControlItem(control_{itemName}_factory);");
                break;
            }
            default:
            {
                // Generate the 'new' expression for the specific SettingsItem type
                var newExpr = metadata.Kind switch
                {
                    ItemKind.Bool => "new global::Everywhere.Configuration.SettingsBooleanItem { IsNullable = false }",
                    ItemKind.NullableBool => "new global::Everywhere.Configuration.SettingsBooleanItem { IsNullable = true }",
                    ItemKind.String => "new global::Everywhere.Configuration.SettingsStringItem()",
                    ItemKind.Int => "new global::Everywhere.Configuration.SettingsIntegerItem()",
                    ItemKind.Double => "new global::Everywhere.Configuration.SettingsDoubleItem()",
                    ItemKind.Enum => "new global::Everywhere.Configuration.SettingsSelectionItem()",
                    _ =>
                        $"global::Everywhere.Configuration.SettingsTypedItem.Create(typeof({metadata.Type.ToDisplayString(NullableFlowState.NotNull)}))"
                };
                sb.Append("var ").Append(itemName).Append(" = ").Append(newExpr).AppendLine(";");
                break;
            }
        }

        // Apply header and description
        ApplyHeaderAndDescription(sb, itemName, metadata);

        // Apply IsEnabled/IsVisible bindings
        ApplySettingsItemAttributes(sb, itemName, metadata);

        // Apply type-specific properties from attributes (e.g., MaxLength)
        ApplyTypeSpecificMetadata(sb, itemName, metadata);

        // Handle nested items via [SettingsItems]
        ApplyGroup(ctx, sb, itemName, metadata, bindingPath);

        sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsItem.ValueProperty] = ");
        EmitBinding(sb, bindingPath, BindingMode.TwoWay).AppendLine(";");

        // Check if the item has [DefaultValue] attribute. If so, wrap it in a SettingsCustomizableItem to enable the Reset button.
        if (metadata.Kind != ItemKind.Customizable &&
            metadata.AttributeOwner.GetAttribute("System.ComponentModel.DefaultValueAttribute") is { ConstructorArguments: [var defaultValue, ..] })
        {
            var defaultValueLiteral = ToLiteral(defaultValue.Value);
            var wrapperItemName = $"{itemName}_wrapper";

            sb.AppendLine($"var {wrapperItemName} = new global::Everywhere.Configuration.SettingsCustomizableItem({itemName});");
            sb.AppendLine($"{wrapperItemName}.ResetCommand = new global::CommunityToolkit.Mvvm.Input.RelayCommand(() =>");
            sb.AppendLine("{");
            using (sb.Indent())
            {
                sb.AppendLine($"this.{bindingPath} = {defaultValueLiteral};");
            }
            sb.AppendLine("});");

            ApplyHeaderAndDescription(sb, wrapperItemName, metadata);

            // Replace itemName with the wrapper
            itemName = wrapperItemName;
        }

        // Add the generated item to its parent collection
        if (!string.IsNullOrEmpty(parentCollection))
        {
            sb.AppendLine($"{parentCollection}.Add({itemName});").AppendLine();
        }
    }

    private static void ApplyHeaderAndDescription(IndentedStringBuilder sb, string itemName, in PropertyMetadata metadata)
    {
        var headerExpr = string.IsNullOrWhiteSpace(metadata.HeaderKey) ?
            "null" :
            $"new global::Everywhere.I18N.DynamicResourceKey({metadata.HeaderKey})";
        sb.AppendLine($"{itemName}.HeaderKey = {headerExpr};");

        if (metadata.DescriptionKey is { Length: > 0 })
        {
            sb.AppendLine($"{itemName}.DescriptionKey = new global::Everywhere.I18N.DynamicResourceKey({metadata.DescriptionKey});");
        }
    }

    /// <summary>
    /// Matches DynamicResourceKey("headerKey", "descriptionKey"), supports multi-line and spaces, e.g.
    /// DynamicResourceKey(
    ///     LocaleKey.CustomAssistant_Icon_Header,
    ///     LocaleKey.CustomAssistant_Icon_Description)
    /// </summary>
    /// <remarks>
    /// We need to parse the syntax ourselves because sometimes the attribute arguments are also SourceGenerated,
    /// and thus we cannot rely on ConstantValue of the attribute data.
    /// </remarks>
    private static readonly Regex DynamicResourceKeyRegex = new(
        @"DynamicResourceKey\s*\(\s*(?<headerKey>[^,)\r\n]+)(\s*,\s*(?<descriptionKey>[^)\r\n]+))?\s*\)",
        RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.Singleline);

    private static PropertyMetadata BuildPropertyMetadata(ISymbol symbol, ISymbol attributeOwner, string name, ITypeSymbol? type = null)
    {
        type ??= ((IPropertySymbol)symbol).Type;
        var kind = Classify(symbol, type);
        var dynamicResourceKeyAttribute = attributeOwner.GetAttribute(KnownAttributes.DynamicResourceKey);
        var code = dynamicResourceKeyAttribute?.ApplicationSyntaxReference?.GetSyntax().ToString();
        var match = code is not null ? DynamicResourceKeyRegex.Match(code) : null;
        var headerKey = match?.Groups["headerKey"].Value.Trim() ?? string.Empty;
        var descriptionKey = match?.Groups["descriptionKey"].Success == true ? match.Groups["descriptionKey"].Value.Trim() : null;
        return new PropertyMetadata(symbol, attributeOwner, name, kind, type, headerKey, descriptionKey);
    }

    private static void ApplyTypeSpecificMetadata(IndentedStringBuilder sb, string itemName, in PropertyMetadata metadata)
    {
        switch (metadata.Kind)
        {
            case ItemKind.String when metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsStringItem) is { } attribute:
            {
                sb.AppendLine($"{itemName}.Watermark = {GetNamedArgValue(attribute, "Watermark", "null")};");
                sb.AppendLine($"{itemName}.MaxLength = {GetNamedArgValue(attribute, "MaxLength", "int.MaxValue")};");
                sb.AppendLine($"{itemName}.IsMultiline = {GetNamedArgValue(attribute, "IsMultiline", "false")};");
                sb.AppendLine(
                    $"{itemName}.TextWrapping = {GetNamedArgValue(attribute, "IsMultiline", false, "global::Avalonia.Media.TextWrapping.Wrap", "global::Avalonia.Media.TextWrapping.NoWrap")};");
                sb.AppendLine($"{itemName}.PasswordChar = {GetNamedArgValue(attribute, "IsPassword", false, "'*'", "'\\0'")};");
                sb.AppendLine($"{itemName}.Height = {GetNamedArgValue(attribute, "Height", "double.NaN")};");
                sb.AppendLine($"{itemName}.MinWidth = {GetNamedArgValue(attribute, "MinWidth", "320d")};");
                break;
            }
            case ItemKind.Int when metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsIntegerItem) is { } attribute:
            {
                sb.AppendLine($"{itemName}.MinValue = {GetNamedArgValue(attribute, "Min", "int.MinValue")};");
                sb.AppendLine($"{itemName}.MaxValue = {GetNamedArgValue(attribute, "Max", "int.MaxValue")};");
                sb.AppendLine($"{itemName}.IsSliderVisible = {GetNamedArgValue(attribute, "IsSliderVisible", "true")};");
                sb.AppendLine($"{itemName}.IsTextBoxVisible = {GetNamedArgValue(attribute, "IsTextBoxVisible", "true")};");
                break;
            }
            case ItemKind.Double when metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsDoubleItem) is { } attribute:
            {
                sb.AppendLine($"{itemName}.MinValue = {GetNamedArgValue(attribute, "Min", "double.NegativeInfinity")};");
                sb.AppendLine($"{itemName}.MaxValue = {GetNamedArgValue(attribute, "Max", "double.PositiveInfinity")};");
                sb.AppendLine($"{itemName}.Step = {GetNamedArgValue(attribute, "Step", "0.1d")};");
                sb.AppendLine($"{itemName}.IsSliderVisible = {GetNamedArgValue(attribute, "IsSliderVisible", "true")};");
                sb.AppendLine($"{itemName}.IsTextBoxVisible = {GetNamedArgValue(attribute, "IsTextBoxVisible", "true")};");
                break;
            }
            case ItemKind.Enum:
            {
                var enumTypeStr = metadata.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

                sb.AppendLine(
                    $"{itemName}.ItemsSource = new global::System.Collections.Generic.List<global::Everywhere.Configuration.SettingsSelectionItem.Item>");
                sb.AppendLine("{");
                using (sb.Indent())
                {
                    var enumMembers = metadata.Type.GetMembers()
                        .OfType<IFieldSymbol>()
                        .Where(f => f.IsConst); // Enum members are constants

                    foreach (var member in enumMembers)
                    {
                        var memberAccess = $"{enumTypeStr}.{member.Name}";
                        // Check for [DynamicResourceKey] on the enum member
                        string? headerKey = null;
                        if (member.GetAttribute(KnownAttributes.DynamicResourceKey) is { } attr)
                        {
                            var code = attr.ApplicationSyntaxReference?.GetSyntax().ToString();
                            if (code is not null)
                            {
                                var match = DynamicResourceKeyRegex.Match(code);
                                headerKey = match.Groups["headerKey"].Value.Trim();
                            }
                            else if (attr.ConstructorArguments.Length > 0 &&
                                     attr.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } headerKeyValue)
                            {
                                headerKey = $"\"{headerKeyValue}\"";
                            }
                        }

                        var resourceKeyExpr = headerKey is not null ?
                            $"new global::Everywhere.I18N.DynamicResourceKey({headerKey})" :
                            $"new global::Everywhere.I18N.DirectResourceKey(\"{metadata.Type.Name}_{member.Name}\")";

                        sb.AppendLine($"new global::Everywhere.Configuration.SettingsSelectionItem.Item({resourceKeyExpr}, {memberAccess}, null),");
                    }
                }
                sb.AppendLine("};");
                break;
            }
        }
    }

    private static void ApplySettingsItemAttributes(
        IndentedStringBuilder sb,
        string itemName,
        in PropertyMetadata metadata)
    {
        if (metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsItem) is not { } settingsItemAttribute) return;

        if (settingsItemAttribute.GetNamedArgument("Classes") is { IsNull: false, Values: { Length: > 0 } classesArray })
        {
            var classes = classesArray
                .Where(c => c.Value is string)
                .Select(c => $"\"{c.Value!.ToString()!.Replace("\"", "\\\"")}\"");
            sb.AppendLine($"{itemName}.Classes.AddRange(new string[] {{ {string.Join(", ", classes)} }});");
        }

        if (settingsItemAttribute.GetNamedArgument("IsEnabledBindingPath") is { IsNull: false, Value: string isEnabledBindingPath })
        {
            sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsItem.IsEnabledProperty] = ");
            EmitBinding(sb, isEnabledBindingPath, BindingMode.OneWay).AppendLine(";");
        }

        if (settingsItemAttribute.GetNamedArgument("IsVisibleBindingPath") is { IsNull: false, Value: string isVisibleBindingPath })
        {
            sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsItem.IsVisibleProperty] = ");
            EmitBinding(sb, isVisibleBindingPath, BindingMode.OneWay).AppendLine(";");
        }
    }

    private static void ApplyGroup(
        in SourceProductionContext ctx,
        IndentedStringBuilder sb,
        string itemName,
        in PropertyMetadata metadata,
        string fullBindingPath)
    {
        if (metadata.AttributeOwner.GetAttribute(KnownAttributes.SettingsItems) is not { } settingsItemsAttribute) return;

        sb.AppendLine($"{itemName}.IsExpanded = {GetNamedArgValue(settingsItemsAttribute, "IsExpanded", "false")};");

        sb.Append($"{itemName}[!global::Everywhere.Configuration.SettingsItem.IsExpandableProperty] = ");
        EmitBinding(sb, metadata.Name, BindingMode.OneWay, "global::Avalonia.Data.Converters.ObjectConverters.IsNotNull");
        sb.AppendLine(";").AppendLine();

        var nestedProperties = metadata.Type.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p is { IsStatic: false, GetMethod: not null, IsImplicitlyDeclared: false })
            .Where(p => !p.IsHiddenItem())
            .Select(p => BuildPropertyMetadata(p, p, p.Name))
            .ToImmutableArray();

        foreach (var nestedMeta in nestedProperties)
        {
            var nestedItemName = $"{itemName}_{nestedMeta.Name.Replace(".", "_")}";
            // The new path is the current full path + the nested property name.
            var nestedPath = $"{fullBindingPath}.{nestedMeta.Name}";
            EmitItemRecursive(ctx, sb, nestedMeta, nestedItemName, nestedPath, $"{itemName}.Items");
        }
    }

    /// <summary>
    /// Emit the whole IBinding that represents the binding logic.
    /// for simple paths, this is just a Binding; for complex logical expressions, this may involve MultiBindings.
    /// The source of the binding is always `this`.
    /// So it can observe the whole path from the root object.
    /// e.g.
    /// new CompiledBinding
    /// {
    ///     Path = "SelectedWebSearchEngineProvider.EndPoint.ActualValue",
    ///     Source = this,
    ///     Mode = global::Avalonia.Data.BindingMode.TwoWay,
    ///     Converter = ...
    /// }
    /// </summary>
    private static IndentedStringBuilder EmitBinding(IndentedStringBuilder sb, string path, BindingMode mode, string? converter = null)
    {
        // This method translates a binding path string (which can be a logical expression)
        // into C# code that creates the appropriate Avalonia Binding or MultiBinding.
        void EmitExpression(string expression)
        {
            expression = expression.Trim();

            // Recursively parse logical OR expressions
            if (TryParseLogicalOperator(expression, "||", out var leftOr, out var rightOr))
            {
                sb.AppendLine("new global::Avalonia.Data.MultiBinding");
                sb.AppendLine("{");
                using (sb.Indent())
                {
                    sb.AppendLine("Converter = global::Avalonia.Data.Converters.BoolConverters.Or,");
                    sb.AppendLine("Mode = global::Avalonia.Data.BindingMode.OneWay,");
                    sb.AppendLine("Bindings =");
                    sb.AppendLine("{");
                    using (sb.Indent())
                    {
                        EmitExpression(leftOr);
                        sb.AppendLine(",");
                        EmitExpression(rightOr);
                        sb.AppendLine();
                    }
                    sb.AppendLine("}");
                }
                sb.Append("}");
                return;
            }

            // Recursively parse logical AND expressions
            if (TryParseLogicalOperator(expression, "&&", out var leftAnd, out var rightAnd))
            {
                sb.AppendLine("new global::Avalonia.Data.MultiBinding");
                sb.AppendLine("{");
                using (sb.Indent())
                {
                    sb.AppendLine("Converter = global::Avalonia.Data.Converters.BoolConverters.And,");
                    sb.AppendLine("Mode = global::Avalonia.Data.BindingMode.OneWay,");
                    sb.AppendLine("Bindings =");
                    sb.AppendLine("{");
                    using (sb.Indent())
                    {
                        EmitExpression(leftAnd);
                        sb.AppendLine(",");
                        EmitExpression(rightAnd);
                        sb.AppendLine();
                    }
                    sb.AppendLine("}");
                }
                sb.Append("}");
                return;
            }

            // Handle logical NOT
            if (expression.StartsWith("!"))
            {
                sb.AppendLine("new global::Avalonia.Data.MultiBinding");
                sb.AppendLine("{");
                using (sb.Indent())
                {
                    sb.AppendLine("Converter = new global::Avalonia.Data.Converters.FuncMultiValueConverter<bool, bool>(x => x.Any(y => !y)),");
                    sb.AppendLine("Bindings = ");
                    sb.AppendLine("{");
                    using (sb.Indent())
                    {
                        EmitExpression(expression[1..]);
                        sb.AppendLine(",");
                    }
                    sb.AppendLine("}");
                }
                sb.Append("}");
                return;
            }

            // Base case: a simple property path
            sb.Append($"new global::Avalonia.Data.Binding(\"{expression}\") {{ Source = this }}");
        }

        path = path.Trim();
        if (!path.Contains("||") && !path.Contains("&&") && !path.StartsWith("!"))
        {
            // Simple path, generate a direct binding
            sb.AppendLine($"new global::Avalonia.Data.Binding(\"{path}\")").AppendLine("{");
            using (sb.Indent())
            {
                sb.AppendLine("Source = this,");
                sb.Append("Mode = global::Avalonia.Data.BindingMode.").Append(mode.ToString()).AppendLine(",");
                if (converter is not null) sb.Append("Converter = ").AppendLine(converter);
            }
            sb.Append("}");
        }
        else
        {
            // Complex logical path, start recursive parsing
            EmitExpression(path);
        }

        return sb;
    }

    private static bool TryParseLogicalOperator(string expression, string op, out string left, out string right)
    {
        left = string.Empty;
        right = string.Empty;
        var paren = 0;
        for (var i = expression.Length - 1; i >= 0; i--)
        {
            var c = expression[i];
            switch (c)
            {
                case ')': paren++; break;
                case '(': paren--; break;
            }

            if (paren == 0 && expression[i..].StartsWith(op))
            {
                left = expression[..i].Trim();
                right = expression[(i + op.Length)..].Trim();
                return true;
            }
        }
        return false;
    }

    private static string GetNamedArgValue(AttributeData attribute, string key, string defaultValue)
    {
        return attribute.GetNamedArgument(key) switch
        {
            { IsNull: false } arg => ToLiteral(arg.Value) ?? defaultValue,
            _ => defaultValue
        };
    }

    private static string GetNamedArgValue(AttributeData attribute, string key, bool defaultValue, string trueValue, string falseValue)
    {
        return attribute.GetNamedArgument(key) switch
        {
            { IsNull: false, Value: bool b } => b ? trueValue : falseValue,
            _ => defaultValue ? trueValue : falseValue
        };
    }

    private static string? ToLiteral(object? value)
    {
        return value switch
        {
            null => "null",
            string s => $"\"{s.Replace("\"", "\\\"")}\"",
            bool b => b ? "true" : "false",
            double d => $"{d}d",
            float f => $"{f}f",
            _ => value.ToString()
        };
    }

    private static ItemKind Classify(ISymbol symbol, ITypeSymbol type)
    {
        if (symbol.GetAttribute(KnownAttributes.SettingsSelectionItem) is not null)
        {
            return ItemKind.Selection;
        }

        if (type.AllInterfaces.Any(i => i.ToDisplayString() == "Everywhere.Configuration.ISettingsControl"))
        {
            return ItemKind.SettingsControl;
        }

        return type switch
        {
            INamedTypeSymbol { IsGenericType: true } nts when
                nts.ConstructedFrom.ToDisplayString() == "Everywhere.Configuration.Customizable<T>" => ItemKind.Customizable,
            { SpecialType: SpecialType.System_Boolean } => ItemKind.Bool,
            {
                OriginalDefinition: INamedTypeSymbol
                {
                    SpecialType: SpecialType.System_Nullable_T, TypeArguments: [{ SpecialType: SpecialType.System_Boolean }]
                }
            } => ItemKind.NullableBool,
            { SpecialType: SpecialType.System_String } => ItemKind.String,
            { SpecialType: SpecialType.System_Int32 } => ItemKind.Int,
            { SpecialType: SpecialType.System_Double } or { SpecialType: SpecialType.System_Single } => ItemKind.Double,
            { TypeKind: TypeKind.Enum } => ItemKind.Enum,
            _ => ItemKind.Unknown
        };
    }

    private sealed class IndentedStringBuilder
    {
        private readonly StringBuilder _stringBuilder = new();
        private string _indent = string.Empty;
        private bool _isLineStart = true;

        public IDisposable Indent()
        {
            var previousIndent = _indent;
            _indent += "    ";
            return new DisposableAction(() => _indent = previousIndent);
        }

        public IndentedStringBuilder Append(string text)
        {
            if (_isLineStart) _stringBuilder.Append(_indent);
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
            var lines = text.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
#pragma warning restore RS1035 // Do not use APIs banned for analyzers
            switch (lines.Length)
            {
                case 0:
                {
                    break;
                }
                case 1:
                {
                    _stringBuilder.Append(lines[0]);
                    break;
                }
                default:
                {
                    _stringBuilder.AppendLine(lines[0]);
                    for (var i = 1; i < lines.Length - 1; i++)
                    {
                        _stringBuilder.Append(_indent).AppendLine(lines[i]);
                    }
                    _stringBuilder.Append(_indent).Append(lines[^1]);
                    break;
                }
            }
            _isLineStart = false;
            return this;
        }

        public IndentedStringBuilder AppendLine(string line)
        {
            if (_isLineStart) _stringBuilder.Append(_indent);
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
            var lines = line.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
#pragma warning restore RS1035 // Do not use APIs banned for analyzers
            switch (lines.Length)
            {
                case 0:
                {
                    break;
                }
                case 1:
                {
                    _stringBuilder.AppendLine(lines[0]);
                    break;
                }
                default:
                {
                    _stringBuilder.AppendLine(lines[0]);
                    for (var i = 1; i < lines.Length; i++)
                    {
                        _stringBuilder.Append(_indent).AppendLine(lines[i]);
                    }
                    break;
                }
            }
            _isLineStart = true;
            return this;
        }

        public IndentedStringBuilder AppendLine()
        {
            _stringBuilder.AppendLine();
            _isLineStart = true;
            return this;
        }

        public override string ToString() => _stringBuilder.ToString();

        private sealed class DisposableAction(Action action) : IDisposable
        {
            public void Dispose() => action();
        }
    }

    private enum ItemKind
    {
        Selection,
        Bool,
        NullableBool,
        String,
        Int,
        Double,
        Customizable,
        Enum,
        SettingsControl,
        Unknown
    }

    private readonly record struct PropertyMetadata(
        ISymbol Symbol,
        ISymbol AttributeOwner,
        string Name,
        ItemKind Kind,
        ITypeSymbol Type,
        string HeaderKey,
        string? DescriptionKey
    );

    private enum BindingMode
    {
        OneWay,
        OneWayToSource,
        TwoWay,
        OneTime
    }
}
